// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library dart2js.constants.expressions.evaluate_test;

import 'dart:async';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:compiler/src/constants/expressions.dart';
import 'package:compiler/src/constants/values.dart';
import 'package:compiler/src/constant_system_dart.dart';
import 'package:compiler/src/core_types.dart';
import 'package:compiler/src/dart2jslib.dart';
import 'package:compiler/src/elements/elements.dart';
import 'memory_compiler.dart';

class TestData {
  /// Declarations needed for the [constants].
  final String declarations;
  /// Tested constants.
  final List constants;

  const TestData(this.declarations, this.constants);
}

class ConstantData {
  /// Source code for the constant expression.
  final String code;
  /// Map from environment to expected constant value as structured text.
  final Map<Map<String, String>, String> expectedValues;

  const ConstantData(this.code,
                     this.expectedValues);
}

class MemoryEnvironment implements Environment {
  final Compiler compiler;
  final Map<String, String> env;

  MemoryEnvironment(this.compiler, [this.env = const <String, String>{}]);

  @override
  String readFromEnvironment(String name) => env[name];
}

const List<TestData> DATA = const [
  const TestData('', const [
    const ConstantData('null', const { const {} : 'NullConstant' }),
    const ConstantData('false', const { const {} : 'BoolConstant(false)' }),
    const ConstantData('true', const { const {} : 'BoolConstant(true)' }),
    const ConstantData('0', const { const {} : 'IntConstant(0)' }),
    const ConstantData('0.0', const { const {} : 'DoubleConstant(0.0)' }),
    const ConstantData('"foo"', const { const {} : 'StringConstant("foo")' }),
    const ConstantData('1 + 2', const { const {} : 'IntConstant(3)' }),
    const ConstantData('-(1)', const { const {} : 'IntConstant(-1)' }),
    const ConstantData('identical(0, 1)',
                       const { const {} : 'BoolConstant(false)' }),
    const ConstantData('"a" "b"', const { const {} : 'StringConstant("ab")' }),
    const ConstantData('identical',
        const { const {} : 'FunctionConstant(identical)' }),
    const ConstantData('true ? 0 : 1', const { const {} : 'IntConstant(0)' }),
    const ConstantData('proxy',
        const { const {} : 'ConstructedConstant(_Proxy())' }),
    const ConstantData('Object', const { const {} : 'TypeConstant(Object)' }),
    const ConstantData('const [0, 1]',
        const { const {} : 'ListConstant([IntConstant(0), IntConstant(1)])' }),
    const ConstantData('const <int>[0, 1]', const {
        const {} : 'ListConstant(<int>[IntConstant(0), IntConstant(1)])' }),
    const ConstantData('const {0: 1, 2: 3}',
        const { const {} :
          'MapConstant({IntConstant(0): IntConstant(1), '
                       'IntConstant(2): IntConstant(3)})' }),
    const ConstantData('const <int, int>{0: 1, 2: 3}',
        const { const {} :
          'MapConstant(<int, int>{IntConstant(0): IntConstant(1), '
                                 'IntConstant(2): IntConstant(3)})' }),
    const ConstantData(
        'const bool.fromEnvironment("foo", defaultValue: false)',
        const { const {} : 'BoolConstant(false)',
                const {'foo': 'true'} : 'BoolConstant(true)'}),
    const ConstantData(
        'const int.fromEnvironment("foo", defaultValue: 42)',
        const { const {} : 'IntConstant(42)',
                const {'foo': '87'} : 'IntConstant(87)'}),
    const ConstantData(
        'const String.fromEnvironment("foo", defaultValue: "bar")',
        const { const {} : 'StringConstant("bar")',
                const {'foo': 'foo'} : 'StringConstant("foo")'}),
  ]),
  const TestData('''
const a = const bool.fromEnvironment("foo", defaultValue: true);
const b = const int.fromEnvironment("bar", defaultValue: 42);

class A {
  const A();
}
class B {
  final field1;
  const B(this.field1);
}
class C extends B {
  final field2;
  const C({field1: 42, this.field2: false}) : super(field1);
  const C.named([field = false]) : this(field1: field, field2: field);
}
''', const [
    const ConstantData('const Object()',
        const { const {} : 'ConstructedConstant(Object())' }),
    const ConstantData('const A()',
        const { const {} : 'ConstructedConstant(A())' }),
    const ConstantData('const B(0)',
        const { const {} : 'ConstructedConstant(B(field1=IntConstant(0)))' }),
    const ConstantData('const B(const A())',
        const { const {} :
          'ConstructedConstant(B(field1=ConstructedConstant(A())))' }),
    const ConstantData('const C()', const { const {} :
        'ConstructedConstant(C(field1=IntConstant(42),'
                              'field2=BoolConstant(false)))' }),
    const ConstantData('const C(field1: 87)', const { const {} :
        'ConstructedConstant(C(field1=IntConstant(87),'
                              'field2=BoolConstant(false)))' }),
    const ConstantData('const C(field2: true)', const { const {} :
        'ConstructedConstant(C(field1=IntConstant(42),'
                              'field2=BoolConstant(true)))' }),
    const ConstantData('const C.named()', const { const {} :
        'ConstructedConstant(C(field1=BoolConstant(false),'
                              'field2=BoolConstant(false)))' }),
    const ConstantData('const C.named(87)', const { const {} :
        'ConstructedConstant(C(field1=IntConstant(87),'
                              'field2=IntConstant(87)))' }),
    const ConstantData('const C(field1: a, field2: b)', const {
        const {} :
          'ConstructedConstant(C(field1=BoolConstant(true),'
                                'field2=IntConstant(42)))',
        const {'foo': 'false', 'bar': '87'} :
          'ConstructedConstant(C(field1=BoolConstant(false),'
                                'field2=IntConstant(87)))', }),
  ]),
  const TestData('''
class A<T> implements B {
  final field1;
  const A({this.field1:42});
}
class B<S> implements C {
  const factory B({field1}) = A<B<S>>;
  // TODO(johnniwinther): Enable this when the constructor evaluator doesn't
  // crash:
  /*const factory B.named() = A<S>;*/
}
class C<U> {
  const factory C({field1}) = A<B<double>>;
}
''', const [
    const ConstantData('const A()',
        const { const {} :
          'ConstructedConstant(A<dynamic>(field1=IntConstant(42)))' }),
    const ConstantData('const A<int>(field1: 87)',
        const { const {} :
          'ConstructedConstant(A<int>(field1=IntConstant(87)))' }),
    const ConstantData('const B()',
        const { const {} :
          'ConstructedConstant(A<B<dynamic>>(field1=IntConstant(42)))' }),
    const ConstantData('const B<int>()',
        const { const {} :
          'ConstructedConstant(A<B<int>>(field1=IntConstant(42)))' }),
    const ConstantData('const B<int>(field1: 87)',
        const { const {} :
          'ConstructedConstant(A<B<int>>(field1=IntConstant(87)))' }),
    const ConstantData('const C<int>(field1: 87)',
        const { const {} :
          'ConstructedConstant(A<B<double>>(field1=IntConstant(87)))' }),
    // TODO(johnniwinther): Enable this when the constructor evaluator doesn't
    // crash:
    /*const ConstantData('const B<int>.named()',
        const { const {} :
          'ConstructedConstant(A<int>(field1=IntConstant(42)))' }),*/
  ]),
  const TestData('''
const c = const int.fromEnvironment("foo", defaultValue: 5);
const d = const int.fromEnvironment("bar", defaultValue: 10);

class A {
  final field;
  const A(a, b) : field = a + b;
}

class B extends A {
  const B(a) : super(a, a * 2);
}
''', const [
    const ConstantData('const A(c, d)', const {
        const {} :
          'ConstructedConstant(A(field=IntConstant(15)))',
        const {'foo': '7', 'bar': '11'} :
          'ConstructedConstant(A(field=IntConstant(18)))', }),
    const ConstantData('const B(d)', const {
        const {} :
          'ConstructedConstant(B(field=IntConstant(30)))',
        const {'bar': '42'} :
          'ConstructedConstant(B(field=IntConstant(126)))', }),
  ]),
];

main() {
  asyncTest(() => Future.forEach(DATA, testData));
}

Future testData(TestData data) {
  StringBuffer sb = new StringBuffer();
  sb.write('${data.declarations}\n');
  Map constants = {};
  data.constants.forEach((ConstantData constantData) {
    String name = 'c${constants.length}';
    sb.write('const $name = ${constantData.code};\n');
    constants[name] = constantData;
  });
  sb.write('main() {}\n');
  String source = sb.toString();
  Compiler compiler = compilerFor(
      {'main.dart': source}, options: ['--analyze-all']);
  return compiler.runCompiler(Uri.parse('memory:main.dart')).then((_) {
    var library = compiler.mainApp;
    constants.forEach((String name, ConstantData data) {
      FieldElement field = library.localLookup(name);
      ConstantExpression constant = field.constant;
      data.expectedValues.forEach(
          (Map<String, String> env, String expectedText) {
        Environment environment = new MemoryEnvironment(compiler, env);
        ConstantValue value =
            constant.evaluate(environment, DART_CONSTANT_SYSTEM);
        String valueText = value.toStructuredString();
        Expect.equals(expectedText, valueText,
            "Unexpected value '${valueText}' for contant "
            "`${constant.getText()}`, expected '${expectedText}'.");
      });
    });
  });
}
